use std::{
    future::{ready, Ready},
    mem::MaybeUninit,
    sync::Arc,
    time::Duration,
};

use actix_web::{dev::Payload, FromRequest, HttpRequest};
use dashmap::DashMap;
use tokio::time::sleep;

use crate::{
    error::{AppErr, SERVICE_NOT_LOGIN, SERVICE_NO_TOKEN, SERVICE_TOKEN_INVALID},
    store::user::User,
    utils::current_timestamp,
};

const TOKEN_HEAD: &'static str = "token";
static mut SESSION_MAP: MaybeUninit<DashMap<String, Session>> = MaybeUninit::uninit();
const LOGIN_SEC: u64 = 60 * 60 * 2;
const LOGIN_TIMEOUT: Duration = Duration::from_secs(LOGIN_SEC);

#[derive(Debug, Clone)]
pub struct Session {
    pub user: Arc<User>,
    pub login_timestamp: u64,
}

fn get_session_map() -> &'static DashMap<String, Session> {
    unsafe { SESSION_MAP.assume_init_ref() }
}

pub fn init() {
    unsafe {
        SESSION_MAP.write(DashMap::new());
    }
    tokio::spawn(login_timeout_handler());
}

async fn login_timeout_handler() {
    loop {
        sleep(LOGIN_TIMEOUT).await;
        let cur = current_timestamp();
        get_session_map().retain(|_, v| (v.login_timestamp + LOGIN_SEC) > cur);
    }
}

fn get_session(req: &HttpRequest) -> Result<Session, AppErr> {
    let token = req
        .headers()
        .get(TOKEN_HEAD)
        .ok_or(SERVICE_NO_TOKEN.into_err())?
        .to_str()
        .map_err(|_| SERVICE_TOKEN_INVALID.into_err())?;

    let session = get_session_map().get_mut(token);
    let mut s = session.ok_or(SERVICE_NOT_LOGIN.into_err())?;
    s.login_timestamp = current_timestamp();

    Ok(s.value().clone())
}

impl FromRequest for Session {
    type Error = AppErr;
    type Future = Ready<Result<Session, AppErr>>;

    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
        ready(get_session(req))
    }
}

pub fn update_session(token: String, user: Arc<User>) {
    let session = Session {
        user,
        login_timestamp: current_timestamp(),
    };
    get_session_map().insert(token, session);
}
